package org.picasaapp.client;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.picasaapp.client.PicasaWeb.*;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.i18n.client.Dictionary;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;

/**
 * The entry point class of the application.
 */
public class PicasaApp implements EntryPoint,
        ValueChangeHandler<java.lang.String> {
    /**
     * A node in an album tree.
     */
    private static class AlbumTreeNode {
        private Album album;
        private List<AlbumTreeNode> children = new ArrayList<AlbumTreeNode>();
        
        public AlbumTreeNode(Album album) {
            this.album = album;
        }
        
        public Album getAlbum() { return album; }
        
        public void addChild(AlbumTreeNode child) {
            children.add(child);
        }
        
        public List<AlbumTreeNode> getChildren() {
            return children;
        }
    }
    
    /**
     * A remote service proxy to talk to the server-side album tree service.
     */
    private final AlbumLinkServiceAsync albumLinkService = GWT
            .create(AlbumLinkService.class);

    /** This tree represents the album hierarchy. */
    private Tree tree;

    /** This label contains the error message. */
    private Label errorLabel;

    /** This panel contains all the thumbnails. */
    private RootPanel thumbnails;

    /** The name of the current user. */
    private String username = "";
    
    /** Show username in URL. This is for the case when the application is used
     *  for displaying albums of multiple users. */
    private boolean showUsername;

    /** The name of the current album. */
    private String albumName;

    /** The JavaScript overlay object for the Picasa albums JSON response. */
    private Albums albums;

    /** The JavaScript overlay object for the Picasa photos JSON response. */
    private Photos photos;

    /** The index of currently displayed photo in {@code photos}. */
    private int photoIndex;
    
    /** Set window title according to the current location. */
    private boolean setWindowTitle;
    
    /** Specifies whether to load the album links. */
    private boolean loadAlbumLinks;
    
    /** The links to the parent albums. */
    private AlbumLink[] links;

    /** The roots of album trees. */
    private List<AlbumTreeNode> roots;

    private void setErrorMessage(String message) {
        errorLabel.setText(message);        
    }
    
    private String getLocation(String albumName) {
        return getLocation(albumName, null);
    }

    private String getLocation(String albumName, String photoID) {
        String location = "";
        if (showUsername) location += username;
        if (albumName != null) {
            if (!location.isEmpty()) location += '/';
            location += albumName;
            if (photoID != null) location += '/' + photoID;
        }
        return location;
    }
    
    private void setWindowTitle(String albumTitle) {
        if (!setWindowTitle) return;
        String title = albums.getAuthorName() + " - Picasa";
        if (albumTitle != null) title = albumTitle + " - " + title; 
        Window.setTitle(title);
    }

    /**
     * Builds the album trees.
     */
    private void buildTrees() {
        try {
            Map<String, AlbumTreeNode> albumIDToNode =
                new LinkedHashMap<String, AlbumTreeNode>();
            for (int i = 0; i < albums.getNumAlbums(); ++i) {
                final Album album = albums.getAlbum(i);
                albumIDToNode.put(album.getID(), new AlbumTreeNode(album));
            }
            if (links != null) {
                for (AlbumLink link: links) {
                    AlbumTreeNode child = albumIDToNode.get(link.getAlbumID());
                    AlbumTreeNode parent =
                        albumIDToNode.get(link.getParentAlbumID());
                    parent.addChild(child);
                    albumIDToNode.remove(link.getAlbumID());
                }
            }
            roots = new ArrayList<AlbumTreeNode>(albumIDToNode.size());
            for (AlbumTreeNode node: albumIDToNode.values())
                roots.add(node);
        }
        finally {
            links = null;
        }
    }

    /**
     * Adds an album thumbnail to the panel.
     */
    public void addAlbumThumbnail(Album album) {
        Image image = new Image();
        image.setUrl(album.getThumbnailURL());
        image.setStyleName("album-thumbnail");
        Hyperlink imageLink = new Hyperlink();
        imageLink.getElement().getFirstChild().appendChild(image.getElement());
        String target = getLocation(album.getName());
        imageLink.setTargetHistoryToken(target);
        FlowPanel mainPanel = new FlowPanel();
        mainPanel.setStyleName("album-thumbnail-block");
        mainPanel.addStyleName("inline-block");
        mainPanel.add(imageLink);
        Hyperlink titleLink = new Hyperlink(album.getTitle(), target);
        titleLink.setStyleName("album-thumbnail-title");
        mainPanel.add(titleLink);
        thumbnails.add(mainPanel);
    }

    /**
     * Adds a photo thumbnail to the panel.
     */
    private void addPhotoThumbnail(Photo photo) {
        Image image = new Image();
        double scaleFactor = photo.getScaleFactor(128, 128);
        image.setPixelSize((int)(photo.getWidth() * scaleFactor), 
                (int)(photo.getHeight() * scaleFactor));
        image.setUrl(photo.getThumbnailURL());
        image.setStyleName("photo-thumbnail");
        Hyperlink imageLink = new Hyperlink();
        imageLink.getElement().getFirstChild().appendChild(image.getElement());
        String target = getLocation(albumName, photo.getID());
        imageLink.setTargetHistoryToken(target);
        imageLink.setStyleName("photo-thumbnail-block");
        imageLink.addStyleName("inline-block");
        thumbnails.add(imageLink);
    }

    private static interface LoadHandler {
        void onLoaded();
    }
    
    private void loadAlbums(final LoadHandler handler) {
        albums = null;
        links = null;
        
        // Request albums for the current user.
        String url = "http://picasaweb.google.com/data/feed/api/" +
            "user/" + (username.isEmpty() ? "default" : username) + "?alt=json";
        PicasaWeb.request(url, new Callback<Albums>() {
                    public void callback(Albums response) {
                        albums = response;
                        if (links != null || !loadAlbumLinks) {
                            buildTrees();
                            handler.onLoaded();
                        }
                    }
                });
        if (!loadAlbumLinks) return;

        // Load album links.
        albumLinkService.getParentLinks(username,
                new AsyncCallback<AlbumLink[]>() {
            public void onSuccess(AlbumLink[] result) {
                links = result;
                if (albums != null) {
                    buildTrees();
                    handler.onLoaded();
                }
            }

            public void onFailure(Throwable caught) {
                setErrorMessage(caught.getMessage());
            }
        });
    }

    /**
     * Loads the albums of the current user.
     */
    private void loadAlbums() {
        loadAlbums(new LoadHandler() {
            public void onLoaded() {
                setWindowTitle(null);
                loadTree();
                thumbnails.clear();
                for (int i = 0; i < albums.getNumAlbums(); ++i)
                    addAlbumThumbnail(albums.getAlbum(i));
            }
        });
    }
    
    /**
     * Loads the photos in the current album.
     */
    private void loadPhotos(final LoadHandler handler) {
        if (albums == null) {
            loadAlbums(new LoadHandler() {
                public void onLoaded() {
                    loadTree();
                    loadPhotos(handler);
                }
            });
            return;
        }

        String url = null;
        Album album = null;
        for (int i = 0; i < albums.getNumAlbums(); ++i) {
            if (albums.getAlbum(i).getName().equals(albumName)) {
                album = albums.getAlbum(i);
                break;
            }
        }
        if (album == null) {
            thumbnails.clear();
            setErrorMessage("No such album: " + albumName);
            return;
        }
        url = album.getURL();
        setWindowTitle(album.getTitle());
        PicasaWeb.request(url, new Callback<Photos>() {
            public void callback(Photos response) {
                thumbnails.clear();
                photos = response;
                for (int i = 0; i < response.getNumPhotos(); ++i)
                    addPhotoThumbnail(response.getPhoto(i));
                handler.onLoaded();
            }
        });
    }
    
    private void addTreeItems(TreeItem item, List<AlbumTreeNode> children) {
        for (AlbumTreeNode node: children) {
            final Album album = node.getAlbum();
            String target = getLocation(album.getName());
            TreeItem child = item.addItem(
                    new Hyperlink(album.getTitle(), target));
            addTreeItems(child, node.getChildren());
        }
    }

    /**
     * Loads the tree of albums of the current user.
     */
    private void loadTree() {
        if (tree == null) return;
        tree.clear();
        TreeItem root = tree.addItem(
                new Hyperlink("Albums", getLocation(null)));
        root.removeItems();
        addTreeItems(root, roots);
        root.setState(true);
    }

    /**
     * Shows the photo with the specified ID.
     * Requires photos to be loaded.
     */
    private void showPhoto(String photoID) {
        Photo photo = null;
        for (int i = 0; i < photos.getNumPhotos(); ++i) {
            final Photo p = photos.getPhoto(i);
            if (p.getID().equals(photoID)) {
                photoIndex = i;
                photo = p;
                break;
            }
        }

        // Create a popup to show the full size image.
        final PopupPanel imagePopup = new PopupPanel(true);
        imagePopup.addCloseHandler(new CloseHandler<PopupPanel>() {
            public void onClose(CloseEvent<PopupPanel> event) {
                if (event.isAutoClosed())
                    hidePhoto();
            }
        });
        // Scale to the size of the browser window's client area.
        double scaleFactor = photo.getScaleFactor(Window.getClientWidth(),
                Window.getClientHeight());
        final int width = (int) (photo.getWidth() * scaleFactor);
        Image image = new Image(photo.getURL(width));
        FocusPanel focusPanel = new FocusPanel(image);
        focusPanel.addKeyDownHandler(new KeyDownHandler() {
            public void onKeyDown(KeyDownEvent event) {
                switch (event.getNativeKeyCode()) {
                case KeyCodes.KEY_RIGHT:
                    showNearbyPhoto(true);
                    break;
                case KeyCodes.KEY_LEFT:
                    showNearbyPhoto(false);
                    break;
                case KeyCodes.KEY_ESCAPE:
                    hidePhoto();
                    break;
                }
            }
        });
        imagePopup.setWidget(focusPanel);
        image.setPixelSize(width, (int) (photo.getHeight() * scaleFactor));
        final Element imageElement = image.getElement();
        focusPanel.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                showNearbyPhoto(event.getRelativeX(imageElement) >= width / 2);
            }
        });

        imagePopup.setGlassEnabled(true);
        imagePopup.center();
        focusPanel.setFocus(true);
    }
    
    /**
     * Hides the currently displayed photo.
     */
    private void hidePhoto() {
        History.newItem(getLocation(albumName));
    }
    
    /**
     * Shows the nearby (next or previous) photo.
     */
    private void showNearbyPhoto(boolean next) {
        if (next) {
            if (photoIndex == photos.getNumPhotos() - 1)
                return;
            ++photoIndex;
        }
        else {
            if (photoIndex == 0)
                return;
            --photoIndex;
        }
        History.newItem(getLocation(albumName,
                photos.getPhoto(photoIndex).getID()));
    }
    
    /**
     * This is the entry point method.
     */
    @Override
    public void onModuleLoad() {
        RootPanel root = RootPanel.get("tree");
        if (root != null) {
            tree = new Tree();
            root.add(tree);
        }
        errorLabel = new Label();
        RootPanel.get("error").add(errorLabel);
        thumbnails = RootPanel.get("thumbnails");

        Dictionary params = Dictionary.getDictionary("picasaapp_params");
        if (params != null) {
            Set<String> keys = params.keySet();
            if (keys.contains("username")) {
                username = params.get("username");
            }
            if (keys.contains("set_window_title")) {
                setWindowTitle = Boolean.parseBoolean(
                        params.get("set_window_title"));
            }
        }
        
        History.addValueChangeHandler(this);
        History.fireCurrentHistoryState();
    }

    /**
     * Changes the locations according to the event value.
     */
    @Override
    public void onValueChange(ValueChangeEvent<String> event) {
        setErrorMessage("");
        
        // The location should be in the form
        // [username[/album-name[/photo-id]]],
        // where [] denotes optional components.
        String location = event.getValue();
        String newUsername;
        String newAlbumName;
        if (showUsername) {
            int slashPos = location.indexOf('/');
            if (slashPos == -1) {
                username = location;
                albumName = null;
                loadAlbums();
                return;
            }
            newUsername = location.substring(0, slashPos);
            newAlbumName = location.substring(slashPos + 1);
        } 
        else {
            if (location.isEmpty()) {
                albumName = null;
                loadAlbums();
                return;
            }
            newUsername = username;
            newAlbumName = location;
        }
        
        int slashPos = newAlbumName.indexOf('/');
        String photoID = null;
        if (slashPos != -1) {
            photoID = newAlbumName.substring(slashPos + 1);
            newAlbumName = newAlbumName.substring(0, slashPos);
        }
        if (!newUsername.equals(username) || !newAlbumName.equals(albumName)) {
            username = newUsername;
            albumName = newAlbumName;
            final String finalPhotoID = photoID; 
            loadPhotos(new LoadHandler() {
                public void onLoaded() {
                    if (finalPhotoID != null)
                        showPhoto(finalPhotoID);
                }
            });
        }
        else if (photoID != null)
            showPhoto(photoID);
    }
}
